Model Connection
This document describes the architecture and implementation of HAWKI's AI model connection system, including the data flow, components, and how to add new AI providers.
Table of Contents
- Architecture Overview
 - Key Components
 - Data Flow
 - Provider Implementation
 - How to Add a New Provider
 - Streaming vs Non-Streaming Requests
 - Error Handling
 - Usage Analytics
 
Architecture Overview
HAWKI's AI integration uses a layered service architecture with dependency injection and factory patterns to process requests to various AI models (OpenAI, GWDG, Google, Ollama, OpenWebUI). The system provides a unified interface for interacting with different AI providers while handling model-specific requirements, streaming capabilities, and usage analytics.
Key Components
The AI connection system is built with a layered architecture consisting of the following components:
Service Layer
- AiService: Main entry point for AI interactions, providing unified methods for model retrieval and request processing
 - AiFactory: Factory service responsible for creating provider instances, model contexts, and managing dependencies
 - UsageAnalyzerService: Tracks and records token usage for analytics and billing purposes
 
Provider Layer
- ModelProviderInterface: Interface defining provider contract for model discovery and configuration
 - ClientInterface: Interface defining client contract for request execution and model status checks
 - AbstractClient: Base implementation providing common request validation and streaming fallback logic
 
Provider Implementations
- OpenAI: OpenAiClient, OpenAiRequestConverter, and specific request handlers
 - GWDG: GwdgClient, GwdgRequestConverter, and specific request handlers
 - Google: GoogleClient, GoogleRequestConverter, and specific request handlers
 - Ollama: OllamaClient, OllamaRequestConverter, and specific request handlers
 - OpenWebUI: OpenWebUiClient, OpenWebUiRequestConverter, and specific request handlers
 
Value Objects
- AiRequest: Immutable request object containing model reference and payload
 - AiResponse: Response object with content, usage data, and completion status
 - AiModel: Model definition with capabilities and context binding
 - TokenUsage: Usage tracking data structure
 
Data Flow
Request Flow
- Entry Point: Client sends request to controller (e.g., 
StreamController) - Service Resolution: Controller calls 
AiService->sendRequest()orsendStreamRequest() - Request Processing: 
AiServiceresolves model and createsAiRequestobject - Model Resolution: 
AiFactoryprovides model instance with bound context - Client Delegation: Request is delegated to model's specific client (e.g., 
OpenAiClient) - Request Conversion: Client uses converter to transform payload to provider format
 - API Communication: Appropriate request handler executes HTTP call to provider API
 - Response Processing: Raw response is converted to standardized 
AiResponseformat - Usage Tracking: Token usage is extracted and recorded via 
UsageAnalyzerService - Response Delivery: Formatted response is returned to client
 
AiRequest Structure
class AiRequest
{
    public ?AiModel $model = null;
    public ?array $payload = null;
}
The payload array contains:
[
    'model' => 'gpt-4o',
    'stream' => true,
    'messages' => [
        [
            'role' => 'user',
            'content' => [
                'text' => 'Hello, how are you?',
                'attachments' => ['uuid1', 'uuid2'] // optional
            ]
        ]
    ],
    'temperature' => 0.7,    // optional
    'top_p' => 1.0,         // optional
    // ... other provider-specific parameters
]
AiResponse Structure
class AiResponse
{
    public array $content;           // Response content with structured format
    public ?TokenUsage $usage;       // Token consumption data
    public bool $isDone = true;      // Completion status (false for streaming chunks)
    public ?string $error = null;    // Error message if any
}
Response content format:
[
    'content' => [
        'text' => 'AI-generated response text'
    ],
    'usage' => [
        'promptTokens' => 123,
        'completionTokens' => 456,
        'totalTokens' => 579
    ],
    'isDone' => true
]
Provider Implementation
The new architecture separates concerns between model providers and clients, with dedicated request converters for payload transformation.
Core Interfaces
ModelProviderInterface - Defines provider contract:
interface ModelProviderInterface
{
    public function getConfig(): ProviderConfig;
    public function getModels(): AiModelCollection;
}
ClientInterface - Defines client contract:
interface ClientInterface
{
    public function sendRequest(AiRequest $request): AiResponse;
    public function sendStreamRequest(AiRequest $request, callable $onData): void;
    public function getStatus(AiModel $model): ModelOnlineStatus;
}
Implementation Pattern
Each provider follows this structure:
- Provider Class (e.g., 
GenericModelProvider): Handles model discovery and configuration - Client Class (e.g., 
OpenAiClient): Manages request execution and delegation - Request Converter (e.g., 
OpenAiRequestConverter): Transforms payloads to provider format - Request Handlers: Specific implementations for streaming/non-streaming requests
 
Example: OpenAI Implementation
class OpenAiClient extends AbstractClient
{
    protected function executeRequest(AiRequest $request): AiResponse
    {
        return (new OpenAiNonStreamingRequest(
            $this->converter->convertRequestToPayload($request)
        ))->execute($request->model);
    }
    
    protected function executeStreamingRequest(AiRequest $request, callable $onData): void
    {
        (new OpenAiStreamingRequest(
            $this->converter->convertRequestToPayload($request),
            $onData
        ))->execute($request->model);
    }
}
Provider Examples
OpenAI Provider
class OpenAIProvider extends BaseAIModelProvider
{
    public function formatPayload(array $rawPayload): array
    {
        // Transform payload to OpenAI format
    }
    
    public function formatResponse($response): array
    {
        // Extract content and usage from OpenAI response
    }
    
    // Other implemented methods...
}
Google Provider
class GoogleProvider extends BaseAIModelProvider
{
    public function formatPayload(array $rawPayload): array
    {
        // Transform payload to Google Gemini format
    }
    
    public function formatResponse($response): array
    {
        // Extract content and usage from Google response
    }
    
    // Other implemented methods...
}
How to Add a New Provider
Adding a new AI provider requires implementing the provider pattern with separate components for model discovery, request handling, and payload conversion.
Implementation Steps
1. Create Provider Directory Structure
For a new provider (e.g., "MyProvider"), create the following structure:
app/Services/AI/Providers/MyProvider/
├── MyProviderClient.php
├── MyProviderRequestConverter.php
└── Request/
    ├── MyProviderNonStreamingRequest.php
    ├── MyProviderStreamingRequest.php
    └── MyProviderUsageTrait.php
2. Implement the Client
<?php
namespace App\Services\AI\Providers\MyProvider;
use App\Services\AI\Providers\AbstractClient;
class MyProviderClient extends AbstractClient
{
    public function __construct(
        private readonly MyProviderRequestConverter $converter
    ) {}
    
    protected function executeRequest(AiRequest $request): AiResponse
    {
        return (new MyProviderNonStreamingRequest(
            $this->converter->convertRequestToPayload($request)
        ))->execute($request->model);
    }
    
    protected function executeStreamingRequest(AiRequest $request, callable $onData): void
    {
        (new MyProviderStreamingRequest(
            $this->converter->convertRequestToPayload($request),
            $onData
        ))->execute($request->model);
    }
    
    protected function resolveStatusList(AiModelStatusCollection $statusCollection): void
    {
        // Implement status checking for your provider's models
    }
}
3. Create Request Converter
<?php
namespace App\Services\AI\Providers\MyProvider;
use App\Services\AI\Value\AiRequest;
class MyProviderRequestConverter
{
    public function convertRequestToPayload(AiRequest $request): array
    {
        $rawPayload = $request->payload;
        
        // Transform HAWKI format to your provider's expected format
        return [
            'model' => $rawPayload['model'],
            'messages' => $this->formatMessages($rawPayload['messages']),
            'stream' => $rawPayload['stream'] ?? false,
            // Add other provider-specific parameters
        ];
    }
    
    private function formatMessages(array $messages): array
    {
        // Convert HAWKI message format to provider format
        return array_map(function($message) {
            return [
                'role' => $message['role'],
                'content' => $message['content']['text'] ?? ''
            ];
        }, $messages);
    }
}
4. Implement Request Handlers
<?php
namespace App\Services\AI\Providers\MyProvider\Request;
use App\Services\AI\Providers\AbstractRequest;
use App\Services\AI\Value\AiModel;
use App\Services\AI\Value\AiResponse;
class MyProviderNonStreamingRequest extends AbstractRequest
{
    use MyProviderUsageTrait;
    
    public function __construct(private array $payload) {}
    
    public function execute(AiModel $model): AiResponse
    {
        return $this->executeNonStreamingRequest(
            model: $model,
            payload: $this->payload,
            dataToResponse: fn(array $data) => new AiResponse(
                content: ['text' => $data['choices'][0]['message']['content'] ?? ''],
                usage: $this->extractUsage($model, $data)
            )
        );
    }
}
5. Update Configuration
Add your new provider to the config/model_providers.php file:
'providers' => [
    'myprovider' => [
        'active' => true,
        'api_key' => env('MYPROVIDER_API_KEY'),
        'api_url' => 'https://api.myprovider.com/v1/chat/completions',
        'ping_url' => 'https://api.myprovider.com/v1/models',
        'models' => [
            [
                'id' => 'my-model-1',
                'label' => 'My Provider Model 1',
                'streamable' => true,
                'capabilities' => ['text', 'image']
            ]
        ]
    ]
]
6. Register with Dependency Container
The AiFactory automatically discovers providers by convention. Ensure your provider class follows the naming pattern:
- Provider directory: 
app/Services/AI/Providers/{ProviderName}/ - Client class: 
{ProviderName}Client - The factory will automatically instantiate and configure your provider when needed.
 
Key Implementation Notes
- Request Validation: The 
AbstractClienthandles request validation automatically - Streaming Fallback: Non-streamable models automatically fall back to regular requests
 - Usage Tracking: Implement the usage trait to extract token consumption data
 - Error Handling: Use the base request class error handling patterns
 - Model Capabilities: Define model capabilities (text, image, document processing) in configuration
 
4. Provider-Specific Considerations
When implementing a new provider, consider these aspects:
- API Format Differences: Understand how the API expects messages and returns responses
 - Streaming Protocol: Implement the correct streaming protocol for the provider
 - Usage Tracking: Extract token usage information correctly
 - Error Handling: Handle provider-specific error responses
 - Model Capabilities: Configure which models support streaming
 
5. Testing Your Provider
After implementing your provider, test it thoroughly:
- Test non-streaming requests
 - Test streaming requests
 - Verify error handling
 - Check usage tracking
 - Test with different message inputs
 - Validate response formatting
 
Streaming vs Non-Streaming Requests
The AI service provides unified methods for both streaming and non-streaming requests with automatic fallback handling.
Non-Streaming Requests
Standard requests wait for the complete response:
// Using AiService
$response = $this->aiService->sendRequest([
    'model' => 'gpt-4o',
    'messages' => $messages
]);
// Returns complete AiResponse with content and usage
echo $response->content['text'];
Streaming Requests
Streaming requests deliver responses in real-time chunks:
// Using AiService with callback
$this->aiService->sendStreamRequest([
    'model' => 'gpt-4o', 
    'stream' => true,
    'messages' => $messages
], function(AiResponse $chunk) {
    if (!$chunk->isDone) {
        echo $chunk->content['text']; // Stream partial content
        flush();
    } else {
        // Final chunk with usage data
        $this->recordUsage($chunk->usage);
    }
});
Automatic Streaming Fallback
If a model doesn't support streaming, the system automatically falls back to non-streaming mode:
// In AbstractClient
public function sendStreamRequest(AiRequest $request, callable $onData): void
{
    if (!$request->model->isStreamable()) {
        // Automatic fallback to non-streaming
        $response = $this->sendRequest($request);
        $onData($response);
        return;
    }
    
    $this->executeStreamingRequest($request, $onData);
}
Error Handling
The system provides comprehensive error handling through multiple layers:
Exception Hierarchy
- AiServiceExceptionInterface: Base interface for all AI service exceptions
 - ModelIdNotAvailableException: Thrown when requested model ID is not available
 - NoModelSetInRequestException: Thrown when request lacks model specification
 - IncorrectClientForRequestedModelException: Thrown when model/client mismatch occurs
 
Request Validation
// Automatic validation in AbstractClient
private function validateRequest(AiRequest $request): void
{
    if ($request->model === null) {
        throw new NoModelSetInRequestException();
    }
    
    // Validates client/model compatibility
    if ($modelClient !== $this) {
        throw new IncorrectClientForRequestedModelException(
            $request->model->getClient(),
            $this
        );
    }
}
Error Response Format
// Errors returned in AiResponse
$response = new AiResponse(
    content: [],
    error: 'Connection failed: timeout after 30s'
);
Usage Analytics
The UsageAnalyzerService continues to track AI model usage, now working with the structured TokenUsage value objects:
Token Usage Structure
class TokenUsage implements JsonSerializable
{
    public function __construct(
        public int $promptTokens,
        public int $completionTokens,
        public int $totalTokens
    ) {}
}
Usage Tracking
// Usage automatically extracted from responses
public function submitUsageRecord(TokenUsage $usage, string $type, string $model, ?string $roomId = null): void
{
    UsageRecord::create([
        'user_id' => Auth::id(),
        'room_id' => $roomId,
        'prompt_tokens' => $usage->promptTokens,
        'completion_tokens' => $usage->completionTokens,
        'total_tokens' => $usage->totalTokens,
        'model' => $model,
        'type' => $type,
    ]);
}
Integration with Responses
Usage is automatically tracked when processing AI responses:
// In request handlers, usage is extracted per provider
protected function extractUsage(AiModel $model, array $data): ?TokenUsage
{
    if (!isset($data['usage'])) {
        return null;
    }
    
    return new TokenUsage(
        promptTokens: $data['usage']['prompt_tokens'] ?? 0,
        completionTokens: $data['usage']['completion_tokens'] ?? 0,
        totalTokens: $data['usage']['total_tokens'] ?? 0
    );
}
Analytics Applications
This structured approach enables:
- Real-time Cost Tracking: Monitor token consumption across models
 - Usage Pattern Analysis: Identify high-usage patterns and optimize
 - Billing Integration: Accurate cost allocation per user/room
 - Performance Monitoring: Track model efficiency and response times
 - Resource Planning: Predict capacity needs based on usage trends